package util;

import static java.io.ObjectStreamConstants.TC_NULL;
import static java.io.ObjectStreamConstants.TC_OBJECT;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ReflectUtils {
    private static final Map<Class<?>, Map<String, Field>> cachedFields = new ConcurrentHashMap<Class<?>, Map<String, Field>>();
    private static final Map<Class<?>, Map<String, AccessibleObject>> cachedMethods = new ConcurrentHashMap<Class<?>, Map<String, AccessibleObject>>();
    
    private static Field field(Class<?> clazz, String name) throws SecurityException, NoSuchFieldException {
        Map<String, Field> classFields = cachedFields.get(clazz);
        if (classFields == null) {
            classFields = new ConcurrentHashMap<String, Field>();
            cachedFields.put(clazz, classFields);
        }
        
        Field result = classFields.get(name);
        if (result != null)
            return result;
        
        NoSuchFieldException exception = null;
        while (result == null && clazz != null)
            try {
                result = clazz.getDeclaredField(name);
            } catch (final NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
                if (exception == null)
                    exception = e;
            }
        if (result == null)
            throw exception;
        result.setAccessible(true);
        classFields.put(name, result);
        return result;
    }
    
    /**
     * @param obj
     *            - object or class
     * @param name
     * @return
     * @see Field#get(Object)
     * @author Mykhaylo Adamovych
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Object obj, String name) {
        try {
            final Class<?> clazz = obj instanceof Class<?>
                    ? (Class<?>) obj
                    : obj.getClass();
            return (T) field(clazz, name).get(obj);
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * @param obj
     *            - object or class
     * @param path
     * @return
     * @author Mykhaylo Adamovych
     */
    @SuppressWarnings("unchecked")
    public static <T> T getByPath(Object obj, String path) {
        Object result = obj;
        final String[] pathElements = path.split("\\.");
        for (final String pathElement : pathElements) {
            if (pathElement.contains("()"))
                result = invoke(result, pathElement.replace("()", ""), new Class<?>[] {}, new Object[] {});
            else
                result = get(result, pathElement);
            if (result == null)
                break;
        }
        return (T) result;
    }
    
    /**
     * @param obj
     *            - object or class
     * @param name
     * @param name
     * @return
     * @author Mykhaylo Adamovych
     */
    public static <T> T invoke(Object obj, String name) {
        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6302954
        return ReflectUtils.<T> invoke(obj, name, new Class<?>[] {}, new Object[] {});
    }
    
    /**
     * @param obj
     *            - object or class
     * @param name
     * @param parameterTypes
     * @param args
     * @return
     * @see Method#invoke(Object, Object...)
     * @author Mykhaylo Adamovych
     */
    @SuppressWarnings("unchecked")
    public static <T> T invoke(Object obj, String name, Class<?>[] parameterTypes, Object... args) {
        try {
            final Class<?> clazz = obj instanceof Class<?>
                    ? (Class<?>) obj
                    : obj.getClass();
            if (name == null)
                return ((Constructor<T>) method(clazz, name, parameterTypes)).newInstance(args);
            return (T) ((Method) method(clazz, name, parameterTypes)).invoke(obj, args);
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * @param clazz
     * @param name
     *            null in case of constructor
     * @param parameterTypes
     * @return
     * @throws SecurityException
     * @throws NoSuchMethodException
     */
    private static AccessibleObject method(Class<?> clazz, String name, Class<?>[] parameterTypes) throws SecurityException, NoSuchMethodException {
        Map<String, AccessibleObject> classMethods = cachedMethods.get(clazz);
        if (classMethods == null) {
            classMethods = new ConcurrentHashMap<String, AccessibleObject>();
            cachedMethods.put(clazz, classMethods);
        }
        
        String methodName = methodName(name, parameterTypes);
        AccessibleObject result = classMethods.get(methodName);
        if (result != null)
            return result;
        
        NoSuchMethodException exception = null;
        while (result == null && clazz != null)
            try {
                if (name == null)
                    result = clazz.getDeclaredConstructor(parameterTypes);
                else
                    result = clazz.getDeclaredMethod(name, parameterTypes);
            } catch (final NoSuchMethodException e) {
                clazz = clazz.getSuperclass();
                if (exception == null)
                    exception = e;
            }
        if (result == null)
            throw exception;
        result.setAccessible(true);
        classMethods.put(methodName, result);
        return result;
    }
    
    private static String methodName(String name, Class<?>[] parameterTypes) {
        return name + Arrays.deepToString(parameterTypes);
    }
    
    /**
     * @param clazz
     * @return
     * @see {@link #newInstance(Class, Object[])}
     */
    public static Object newInstance(Class<?> clazz) {
        return newInstance(clazz, new Class<?>[] {}, new Object[] {});
    }
    
    /**
     * @param clazz
     * @param args
     * @return
     * @see {@link Constructor#newInstance(Object...)}
     */
    public static Object newInstance(Class<?> clazz, Class<?>[] parameterTypes, Object... args) {
        return invoke(clazz, null, parameterTypes, args);
    }
    
    /**
     * @return proxy
     * @see Proxy
     * @author Mykhaylo Adamovych
     */
    @SuppressWarnings("unchecked")
    public static <I> I newProxyInstance(InvocationHandler proxy, Class<?>... interfaces) {
        return (I) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), interfaces, proxy);
    }
    
    /**
     * Usually you need to make needed classes to implement {@link Serializable}.<br>
     * You can use this method if very classes are not under your control, e.g they are generated.<br>
     * If generated class contains non serializable fields you need to write your own implementation with regard of those fields.<br>
     * 
     * @param clazz
     * @param ois
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     * @see #writeObject(Object, ObjectOutputStream)
     * @author Mykhaylo Adamovych
     */
    public static Object readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        try {
            if (ois.readByte() == TC_NULL)
                return null;
            final String className = (String) ois.readObject();
            final Class<?> clazz = Class.forName(className);
            final Object result = clazz.newInstance();
            for (final Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                final int mod = field.getModifiers();
                if (Modifier.isStatic(mod) || Modifier.isTransient(mod))
                    continue;
                field.set(result, ois.readObject());
            }
            return result;
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * @param obj
     *            - object or class
     * @param name
     * @param value
     * @see Field#set(Object, Object)
     * @author Mykhaylo Adamovych
     */
    public static void set(Object obj, String name, Object value) {
        try {
            final Class<?> clazz = obj instanceof Class<?>
                    ? (Class<?>) obj
                    : obj.getClass();
            field(clazz, name).set(obj, value);
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * Usually you need to make needed classes to implement {@link Cloneable} and expose {@link Object#clone()}.<br>
     * You can use this method if very classes are not under your control, e.g they are generated.
     * 
     * @param from
     *            object to be cloned.
     * @return shallow copy of an object.
     * @author Mykhaylo Adamovych
     */
    @SuppressWarnings("unchecked")
    public static <T> T shallowCopy(T from) {
        try {
            final Class<?> clazz = from.getClass();
            final T result = (T) clazz.newInstance();
            for (final Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                field.set(result, field.get(from));
            }
            return result;
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * Usually you need to make needed classes to implement {@link Serializable}.<br>
     * You can use this method if very classes are not under your control, e.g they are generated.<br>
     * If generated class contains non serializable fields you need to write your own implementation with regard of those fields.<br>
     * 
     * @param obj
     * @param oos
     * @throws IOException
     * @see #readObject(Class, ObjectInputStream)
     * @author Mykhaylo Adamovych
     */
    public static void writeObject(final Object obj, ObjectOutputStream oos) throws IOException {
        try {
            if (obj == null) {
                oos.writeByte(TC_NULL);
                return;
            }
            oos.writeByte(TC_OBJECT);
            Class<?> clazz = obj.getClass();
            if (clazz.isAnonymousClass())
                clazz = clazz.getSuperclass();
            oos.writeObject(clazz.getName());
            for (final Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                final int mod = field.getModifiers();
                if (Modifier.isStatic(mod) || Modifier.isTransient(mod))
                    continue;
                oos.writeObject(field.get(obj));
            }
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }
}
